package botcraft.game;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import botcraft.game.unit.Unit;
import botcraft.game.xml.BotcraftErrorHandler;
import botcraft.game.xml.BotcraftNamespaceContext;

public class Game {
	public static final URI NAMESPACE = URI.create("http://code.google.com/p/botcraft/game/");
	public static final String PREFIX = "game";
	private static final String SCHEMA = "/schemas/game.xsd";
	
	private final URL map;
	private final Set<URL> objectPackages;
	private final Set<Object> objects;
	private final Set<Team> teams;
	
	public Game(final URL map, final Set<URL> objectPackages, final Set<Object> objects, final Set<Team> teams) {
		if (map == null)
			throw new IllegalArgumentException("Game map can not be null");
		if (objectPackages == null)
			throw new IllegalArgumentException("Game object packages can not be null");
		if (objects == null)
			throw new IllegalArgumentException("Game objects can not be null");
		if (teams == null)
			throw new IllegalArgumentException("Game teams can not be null");
		this.map = map;
		this.objectPackages = objectPackages;
		this.objects = objects;
		this.teams = teams;
	}
	
	public Game(final ObjectManager objectManager, final URL url) throws IOException {
		try {
			final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			documentBuilderFactory.setNamespaceAware(true);
			documentBuilderFactory.setSchema(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(Game.class.getResource(SCHEMA)));
			documentBuilderFactory.setXIncludeAware(true);
			final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
			documentBuilder.setErrorHandler(new BotcraftErrorHandler());
			final Document document = documentBuilder.parse(url.openStream());
			final XPath xPath = XPathFactory.newInstance().newXPath();
			xPath.setNamespaceContext(new BotcraftNamespaceContext());
			map = new URL(((Node) xPath.evaluate("/" + PREFIX + ":game/" + PREFIX + ":map", document, XPathConstants.NODE)).getTextContent());
			final NodeList gameObjectPackages = (NodeList) xPath.evaluate("/" + PREFIX + ":game/" + PREFIX + ":objectPackages/" + PREFIX + ":objectPackage",
					document, XPathConstants.NODESET);
			objectPackages = new HashSet<URL>();
			for (int i = 0; i < gameObjectPackages.getLength(); i++) {
				final URL objectPackage = new URL(gameObjectPackages.item(i).getTextContent());
				objectManager.loadObjectPackage(objectPackage);
				objectPackages.add(objectPackage);
			}
			final NodeList gameObjects = (NodeList) xPath.evaluate("/" + PREFIX + ":game/" + PREFIX + ":objects/" + PREFIX + ":object", document,
					XPathConstants.NODESET);
			objects = new HashSet<Object>();
			for (int i = 0; i < gameObjects.getLength(); i++) {
				objects.add(objectManager.newObject(((Node) xPath.evaluate("./" + PREFIX + ":class", gameObjects.item(i), XPathConstants.NODE)).getTextContent(),
						UUID.fromString(((Node) xPath.evaluate("./" + PREFIX + ":id", gameObjects.item(i), XPathConstants.NODE)).getTextContent()),
						new Vector(Float.parseFloat(((Node) xPath.evaluate("./" + PREFIX + ":position/" + PREFIX + ":x", gameObjects.item(i),
						XPathConstants.NODE)).getTextContent()), Float.parseFloat(((Node) xPath.evaluate("./" + PREFIX + ":position/" + PREFIX + ":y",
						gameObjects.item(i), XPathConstants.NODE)).getTextContent()), Float.parseFloat(((Node) xPath.evaluate("./" + PREFIX + ":position/" +
						PREFIX + ":z", gameObjects.item(i), XPathConstants.NODE)).getTextContent())), new Vector(Float.parseFloat(((Node) xPath.evaluate("./" +
						PREFIX + ":orientation/" + PREFIX + ":x", gameObjects.item(i), XPathConstants.NODE)).getTextContent()), Float.parseFloat(((Node)
						xPath.evaluate("./" + PREFIX + ":orientation/" + PREFIX + ":y", gameObjects.item(i), XPathConstants.NODE)).getTextContent()),
						Float.parseFloat(((Node) xPath.evaluate("./" + PREFIX + ":orientation/" + PREFIX + ":z", gameObjects.item(i),
						XPathConstants.NODE)).getTextContent()))));
			}
			final NodeList gameTeams = (NodeList) xPath.evaluate("/" + PREFIX + ":game/" + PREFIX + ":teams/" + PREFIX + ":team", document,
					XPathConstants.NODESET);
			teams = new HashSet<Team>();
			for (int i = 0; i < gameTeams.getLength(); i++) {
				final NodeList gamePlayers = (NodeList) xPath.evaluate("./" + PREFIX + ":players/" + PREFIX + ":player", gameTeams.item(i),
						XPathConstants.NODESET);
				final Set<Player> players = new HashSet<Player>();
				for (int j = 0; j < gamePlayers.getLength(); j++) {
					final NodeList gameUnits = (NodeList) xPath.evaluate("./" + PREFIX + ":units/" + PREFIX + ":unit", gamePlayers.item(j),
							XPathConstants.NODESET);
					final Set<Unit> units = new HashSet<Unit>();
					for (int k = 0; k < gameUnits.getLength(); k++) {
						units.add(objectManager.newUnit(((Node) xPath.evaluate("./" + PREFIX + ":class", gameUnits.item(k),
								XPathConstants.NODE)).getTextContent(), UUID.fromString(((Node) xPath.evaluate("./" + PREFIX + ":id", gameUnits.item(k),
								XPathConstants.NODE)).getTextContent()), new Vector(Float.parseFloat(((Node) xPath.evaluate("./" + PREFIX + ":position/" +
								PREFIX + ":x", gameUnits.item(k), XPathConstants.NODE)).getTextContent()), Float.parseFloat(((Node) xPath.evaluate("./" +
								PREFIX + ":position/" + PREFIX + ":y", gameUnits.item(k), XPathConstants.NODE)).getTextContent()), Float.parseFloat(((Node)
								xPath.evaluate("./" + PREFIX + ":position/" + PREFIX + ":z", gameUnits.item(k), XPathConstants.NODE)).getTextContent())),
								new Vector(Float.parseFloat(((Node) xPath.evaluate("./" + PREFIX + ":orientation/" + PREFIX + ":x", gameUnits.item(k),
								XPathConstants.NODE)).getTextContent()), Float.parseFloat(((Node) xPath.evaluate("./" + PREFIX + ":orientation/" + PREFIX + ":y",
								gameUnits.item(k), XPathConstants.NODE)).getTextContent()), Float.parseFloat(((Node) xPath.evaluate("./" + PREFIX +
								":orientation/" + PREFIX + ":z", gameUnits.item(k), XPathConstants.NODE)).getTextContent()))));
					}
					players.add(new Player(((Node) xPath.evaluate("./" + PREFIX + ":name", gamePlayers.item(j), XPathConstants.NODE)).getTextContent(), units));
				}
				teams.add(new Team(((Node) xPath.evaluate("./" + PREFIX + ":name", gameTeams.item(i), XPathConstants.NODE)).getTextContent(), players));
			}
		} catch (final ParserConfigurationException e) {
			throw new IOException("Error loading game " + url, e);
		} catch (final SAXException e) {
			throw new IOException("Error loading game " + url, e);
		} catch (final XPathExpressionException e) {
			throw new IOException("Error loading game " + url, e);
		}
	}
	
	public URL getMap() {
		return map;
	}
	
	public Set<URL> getUnitPackages() {
		return objectPackages;
	}
	
	public Set<Object> getObjects() {
		return objects;
	}
	
	public Set<Team> getTeams() {
		return teams;
	}
	
	public void save(final File file) throws IOException {
		try {
			final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			documentBuilderFactory.setNamespaceAware(true);
			documentBuilderFactory.setValidating(true);
			documentBuilderFactory.setSchema(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(Game.class.getResource(SCHEMA)));
			documentBuilderFactory.setXIncludeAware(true);
			final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
			documentBuilder.setErrorHandler(new BotcraftErrorHandler());
			final Document document = documentBuilder.newDocument();
			final Element gameGame = document.createElementNS(NAMESPACE.toString(), PREFIX + ":game");
			gameGame.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", NAMESPACE + " " + SCHEMA);
			final Element gameMap = document.createElementNS(NAMESPACE.toString(), PREFIX + ":map");
			gameMap.appendChild(document.createTextNode(map.toString()));
			gameGame.appendChild(gameMap);
			final Element gameObjectPackages = document.createElementNS(NAMESPACE.toString(), PREFIX + ":objectPackages");
			for (URL objectPackage : objectPackages) {
				final Element gameObjectPackage = document.createElementNS(NAMESPACE.toString(), PREFIX + ":objectPackage");
				gameObjectPackage.appendChild(document.createTextNode(objectPackage.toString()));
				gameObjectPackages.appendChild(gameObjectPackage);
			}
			gameGame.appendChild(gameObjectPackages);
			final Element gameObjects = document.createElementNS(NAMESPACE.toString(), PREFIX + ":objects");
			for (Object object : objects) {
				final Element gameObject = document.createElementNS(NAMESPACE.toString(), PREFIX + ":object");
				final Element gameClass = document.createElementNS(NAMESPACE.toString(), PREFIX + ":class");
				gameClass.appendChild(document.createTextNode(object.getClass().getName()));
				gameObject.appendChild(gameClass);
				final Element gameId = document.createElementNS(NAMESPACE.toString(), PREFIX + ":id");
				gameId.appendChild(document.createTextNode(object.getId().toString()));
				gameObject.appendChild(gameId);
				final Element gamePosition = document.createElementNS(NAMESPACE.toString(), PREFIX + ":position");
				final Element gamePositionX = document.createElementNS(NAMESPACE.toString(), PREFIX + ":x");
				gamePositionX.appendChild(document.createTextNode(Float.toString(object.getPosition().getX())));
				gamePosition.appendChild(gamePositionX);
				final Element gamePositionY = document.createElementNS(NAMESPACE.toString(), PREFIX + ":y");
				gamePositionY.appendChild(document.createTextNode(Float.toString(object.getPosition().getY())));
				gamePosition.appendChild(gamePositionY);
				final Element gamePositionZ = document.createElementNS(NAMESPACE.toString(), PREFIX + ":z");
				gamePositionZ.appendChild(document.createTextNode(Float.toString(object.getPosition().getZ())));
				gamePosition.appendChild(gamePositionZ);
				gameObject.appendChild(gamePosition);
				final Element gameOrientation = document.createElementNS(NAMESPACE.toString(), PREFIX + ":orientation");
				final Element gameOrientationX = document.createElementNS(NAMESPACE.toString(), PREFIX + ":x");
				gameOrientationX.appendChild(document.createTextNode(Float.toString(object.getOrientation().getX())));
				gameOrientation.appendChild(gameOrientationX);
				final Element gameOrientationY = document.createElementNS(NAMESPACE.toString(), PREFIX + ":y");
				gameOrientationY.appendChild(document.createTextNode(Float.toString(object.getOrientation().getY())));
				gameOrientation.appendChild(gameOrientationY);
				final Element gameOrientationZ = document.createElementNS(NAMESPACE.toString(), PREFIX + ":z");
				gameOrientationZ.appendChild(document.createTextNode(Float.toString(object.getOrientation().getZ())));
				gameOrientation.appendChild(gameOrientationZ);
				gameObject.appendChild(gameOrientation);
				gameObjects.appendChild(gameObject);
			}
			gameGame.appendChild(gameObjects);
			final Element gameTeams = document.createElementNS(NAMESPACE.toString(), PREFIX + ":teams");
			for (Team team : teams) {
				final Element gameTeam = document.createElementNS(NAMESPACE.toString(), PREFIX + ":team");
				final Element gameTeamName = document.createElementNS(NAMESPACE.toString(), PREFIX + ":name");
				gameTeamName.appendChild(document.createTextNode(team.getName()));
				gameTeam.appendChild(gameTeamName);
				final Element gamePlayers = document.createElementNS(NAMESPACE.toString(), PREFIX + ":players");
				for (Player player : team.getPlayers()) {
					final Element gamePlayer = document.createElementNS(NAMESPACE.toString(), PREFIX + ":player");
					final Element gamePlayerName = document.createElementNS(NAMESPACE.toString(), PREFIX + ":name");
					gamePlayerName.appendChild(document.createTextNode(player.getName()));
					gamePlayer.appendChild(gamePlayerName);
					final Element gamePlayerUnits = document.createElementNS(NAMESPACE.toString(), PREFIX + ":units");
					for (Unit unit : player.getUnits()) {
						final Element gameUnit = document.createElementNS(NAMESPACE.toString(), PREFIX + ":unit");
						final Element gameClass = document.createElementNS(NAMESPACE.toString(), PREFIX + ":class");
						gameClass.appendChild(document.createTextNode(unit.getClass().getName()));
						gameUnit.appendChild(gameClass);
						final Element gameId = document.createElementNS(NAMESPACE.toString(), PREFIX + ":id");
						gameId.appendChild(document.createTextNode(unit.getId().toString()));
						gameUnit.appendChild(gameId);
						final Element gamePosition = document.createElementNS(NAMESPACE.toString(), PREFIX + ":position");
						final Element gamePositionX = document.createElementNS(NAMESPACE.toString(), PREFIX + ":x");
						gamePositionX.appendChild(document.createTextNode(Float.toString(unit.getPosition().getX())));
						gamePosition.appendChild(gamePositionX);
						final Element gamePositionY = document.createElementNS(NAMESPACE.toString(), PREFIX + ":y");
						gamePositionY.appendChild(document.createTextNode(Float.toString(unit.getPosition().getY())));
						gamePosition.appendChild(gamePositionY);
						final Element gamePositionZ = document.createElementNS(NAMESPACE.toString(), PREFIX + ":z");
						gamePositionZ.appendChild(document.createTextNode(Float.toString(unit.getPosition().getZ())));
						gamePosition.appendChild(gamePositionZ);
						gameUnit.appendChild(gamePosition);
						final Element gameOrientation = document.createElementNS(NAMESPACE.toString(), PREFIX + ":orientation");
						final Element gameOrientationX = document.createElementNS(NAMESPACE.toString(), PREFIX + ":x");
						gameOrientationX.appendChild(document.createTextNode(Float.toString(unit.getOrientation().getX())));
						gameOrientation.appendChild(gameOrientationX);
						final Element gameOrientationY = document.createElementNS(NAMESPACE.toString(), PREFIX + ":y");
						gameOrientationY.appendChild(document.createTextNode(Float.toString(unit.getOrientation().getY())));
						gameOrientation.appendChild(gameOrientationY);
						final Element gameOrientationZ = document.createElementNS(NAMESPACE.toString(), PREFIX + ":z");
						gameOrientationZ.appendChild(document.createTextNode(Float.toString(unit.getOrientation().getZ())));
						gameOrientation.appendChild(gameOrientationZ);
						gameUnit.appendChild(gameOrientation);
						gamePlayerUnits.appendChild(gameUnit);
					}
					gamePlayer.appendChild(gamePlayerUnits);
					gamePlayers.appendChild(gamePlayer);
				}
				gameTeam.appendChild(gamePlayers);				
				gameTeams.appendChild(gameTeam);
			}
			gameGame.appendChild(gameTeams);
			document.appendChild(gameGame);
			TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document), new StreamResult(file));
		} catch (final SAXException e) {
			throw new IOException("Error saving game to file " + file, e);
		} catch (final ParserConfigurationException e) {
			throw new IOException("Error saving game to file " + file, e);
		} catch (final TransformerException e) {
			throw new IOException("Error saving game to file " + file, e);
		}
	}
}
